Рынок заведений общественного питания Москвы¶

Инвесторы из фонда «Shut Up and Take My Money» решили попробовать себя в новой области и открыть заведение общественного питания в Москве. Заказчики ещё не знают, что это будет за место: кафе, ресторан, пиццерия, паб или бар, — и какими будут расположение, меню и цены.

Цель:
Исследовать рынок Москвы, найти интересные особенности и презентовать полученные результаты, которые в будущем помогут в выборе подходящего инвесторам места.

Задачи:

  1. Изучить сткрутуру данных;
  2. Изучить наличие пропусков, дубкатов и других аспектов, которые могут повлиять на ход работы;
  3. Провести анализ общих данных, выявить закономерности;
  4. Изучить данные о кофейнях, выявить лучшие параметры для её открытия;
  5. Сделать отчёт в виде презентации.

Описание данных:

Доступен датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года.

Файл moscow_places.csv:

name — название заведения;
address — адрес заведения;
category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
hours — информация о днях и часах работы;
lat — широта географической точки, в которой находится заведение;
lng — долгота географической точки, в которой находится заведение;
rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:

  • «Средний счёт: 1000–1500 ₽»;
  • «Цена чашки капучино: 130–220 ₽»;
  • «Цена бокала пива: 400–600 ₽». и так далее;

middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»:

  • Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
  • Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
  • Если значения нет или оно не начинается с подстроки «Средний счёт», то в столбец ничего не войдёт.

middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»:

  • Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
  • Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
  • Если значения нет или оно не начинается с подстроки «Цена одной чашки капучино», то в столбец ничего не войдёт.

chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки);
district — административный район, в котором находится заведение, например Центральный административный округ;
seats — количество посадочных мест.

Загрузим данные и изучим общую информацию¶

In [1]:
import re
import json
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import warnings; warnings.filterwarnings(action='ignore')


from sklearn.impute import KNNImputer
from plotly import graph_objects as go
from folium import Map, Choropleth
from folium import Map, Marker # импортируем карту и маркер
from folium.plugins import MarkerCluster # импортируем кластер
from folium import Map, Choropleth # импортируем карту и хороплет
In [2]:
# снимаем ограничение на количество столбцов
pd.set_option('display.max_columns', None)

# снимаем ограничение на ширину столбцов
pd.set_option('display.max_colwidth', None)

# игнорируем предупреждения
pd.set_option('chained_assignment', None)

# выставляем ограничение на показ знаков после запятой
pd.options.display.float_format = '{:,.2f}'.format

# устанавливаем стиль графиков
sns.set_style('whitegrid')
sns.set(rc={"figure.dpi":200, 'savefig.dpi':300})
sns.set_context('notebook')  
sns.set_style("ticks")
In [3]:
try:
    data = pd.read_csv('/datasets/moscow_places.csv')
except:
    data = pd.read_csv('moscow_places.csv')
In [4]:
display(data.head(), data.sample(5), data.tail())
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
0 WoWфли кафе Москва, улица Дыбенко, 7/1 Северный административный округ ежедневно, 10:00–22:00 55.88 37.48 5.00 NaN NaN NaN NaN 0 NaN
1 Четыре комнаты ресторан Москва, улица Дыбенко, 36, корп. 1 Северный административный округ ежедневно, 10:00–22:00 55.88 37.48 4.50 выше среднего Средний счёт:1500–1600 ₽ 1,550.00 NaN 0 4.00
2 Хазри кафе Москва, Клязьминская улица, 15 Северный административный округ пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00–02:00 55.89 37.53 4.60 средние Средний счёт:от 1000 ₽ 1,000.00 NaN 0 45.00
3 Dormouse Coffee Shop кофейня Москва, улица Маршала Федоренко, 12 Северный административный округ ежедневно, 09:00–22:00 55.88 37.49 5.00 NaN Цена чашки капучино:155–185 ₽ NaN 170.00 0 NaN
4 Иль Марко пиццерия Москва, Правобережная улица, 1Б Северный административный округ ежедневно, 10:00–22:00 55.88 37.45 5.00 средние Средний счёт:400–600 ₽ 500.00 NaN 1 148.00
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
6227 Хлеб с маслом булочная Москва, улица Вавилова, 3 Южный административный округ ежедневно, 09:00–22:00 55.71 37.59 4.80 NaN NaN NaN NaN 1 320.00
4621 ФО Point ресторан Москва, улица Сретенка, 1с1 Центральный административный округ пн-пт 11:00–23:00; сб,вс 12:00–23:00 55.77 37.63 4.40 средние Средний счёт:500–1000 ₽ 750.00 NaN 0 NaN
1820 Ещё одна собачка кофейня Москва, улица Дубки, 2 Северный административный округ ежедневно, 09:00–21:00 55.82 37.57 4.70 NaN Цена чашки капучино:180–250 ₽ NaN 215.00 0 NaN
6831 Столовая кафе Москва, Профсоюзная улица, 83А Юго-Западный административный округ ежедневно, 10:00–18:00 55.65 37.53 4.40 NaN NaN NaN NaN 0 50.00
922 Хей Мам пиццерия Москва, улица Коминтерна, 15 Северо-Восточный административный округ ежедневно, 10:00–23:00 55.87 37.69 4.60 NaN NaN NaN NaN 0 NaN
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
8401 Суши Мания кафе Москва, Профсоюзная улица, 56 Юго-Западный административный округ ежедневно, 09:00–02:00 55.67 37.55 4.40 NaN NaN NaN NaN 0 86.00
8402 Миславнес кафе Москва, Пролетарский проспект, 19, корп. 1 Южный административный округ ежедневно, 08:00–22:00 55.64 37.66 4.80 NaN NaN NaN NaN 0 150.00
8403 Самовар кафе Москва, Люблинская улица, 112А, стр. 1 Юго-Восточный административный округ ежедневно, круглосуточно 55.65 37.74 3.90 NaN Средний счёт:от 150 ₽ 150.00 NaN 0 150.00
8404 Чайхана Sabr кафе Москва, Люблинская улица, 112А, стр. 1 Юго-Восточный административный округ ежедневно, круглосуточно 55.65 37.74 4.20 NaN NaN NaN NaN 1 150.00
8405 Kebab Time кафе Москва, Россошанский проезд, 6 Южный административный округ ежедневно, круглосуточно 55.60 37.60 3.90 NaN NaN NaN NaN 0 12.00
In [5]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8406 entries, 0 to 8405
Data columns (total 14 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   name               8406 non-null   object 
 1   category           8406 non-null   object 
 2   address            8406 non-null   object 
 3   district           8406 non-null   object 
 4   hours              7870 non-null   object 
 5   lat                8406 non-null   float64
 6   lng                8406 non-null   float64
 7   rating             8406 non-null   float64
 8   price              3315 non-null   object 
 9   avg_bill           3816 non-null   object 
 10  middle_avg_bill    3149 non-null   float64
 11  middle_coffee_cup  535 non-null    float64
 12  chain              8406 non-null   int64  
 13  seats              4795 non-null   float64
dtypes: float64(6), int64(1), object(7)
memory usage: 919.5+ KB
In [6]:
data.duplicated().sum()
Out[6]:
0
In [7]:
data.isna().sum()
Out[7]:
name                    0
category                0
address                 0
district                0
hours                 536
lat                     0
lng                     0
rating                  0
price                5091
avg_bill             4590
middle_avg_bill      5257
middle_coffee_cup    7871
chain                   0
seats                3611
dtype: int64
In [8]:
pd.DataFrame(round(data.isna().mean()*100,)).style.background_gradient('coolwarm')
Out[8]:
  0
name 0.000000
category 0.000000
address 0.000000
district 0.000000
hours 6.000000
lat 0.000000
lng 0.000000
rating 0.000000
price 61.000000
avg_bill 55.000000
middle_avg_bill 63.000000
middle_coffee_cup 94.000000
chain 0.000000
seats 43.000000

Комментарий:

Видим пропуски в столбцах hours, price, avg_bill, middle_avg_bill, middle_coffee_cup, seats Пропусков в данных огромное количество. Просто удалить не получится, будут искажения в исследовании. Для дальнейшего исследования нам понадабятся столбец middle_avg_bill. Значения для этого столбца берутся из avg_bill.

In [9]:
print('Количество заведений:', data.name.nunique())
Количество заведений: 5614
In [ ]:
data.category.value_counts()
In [ ]:
data.seats.describe()

Комментарий:

В датасете имеется информация о 5614 заведениях питания в Москве. В столбцах name, category, address, district, hours, price и avg_bill содержится информация о названии, категории, адресе, районе, часах работы, ценовой категории и средней стоимости заказа, представленных в формате объекта. Колонки lat и lng предоставляют данные о географических координатах заведения, а middle_avg_bill и middle_coffee_cup содержат данные о средней стоимости заказа и чашки капучино соответственно. Столбец chain содержит целочисленные значения 1 и 0, указывающие, относится заведение к сети или нет. Однако, столбец seats содержит данные о количестве посадочных мест в виде чисел с плавающей точкой, что является ошибкой. Кроме того, в некоторых столбцах есть пропущенные значения.

Предобработка данных¶

In [ ]:
# Данные выглядят корректно, приведем весь текст в нижний регистр.
for col in ['name', 'address', 'avg_bill']:
    data[col] = data[col].str.lower()
data.head()
In [ ]:
# Воссполним пропуски колонок 'avg_bill', 'price', 'hours'
for column in ['avg_bill', 'price', 'hours']:
    data[column] = data[column].fillna('н/д')

Комментарий:
Столбец middle_avg_bill содержит число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт». Если значения в столбце avg_bill нет или оно не начинается с подстроки «Средний счёт», то в столбец ничего не войдёт.

Выведем количество непустых значений столбца middle_avg_bill.

In [ ]:
len(data.query('~middle_avg_bill.isna()'))

Найдём количество строк датасета data, для которых значение столбца avg_bill начинается с подстроки «cредний счёт».

In [ ]:
len(data.loc[data['avg_bill'].apply(lambda x: x.find('средний счёт')) != -1])

Комментарий:
Число строк 3149 равно количеству непустых значений в столбце middle_avg_bill.

Заполним пропуски c помощью модели машинного обучения метод K-ближайших соседей.

In [ ]:
imputer = KNNImputer()
middle_avg_bill_2d = data['middle_avg_bill'].values.reshape(-1, 1)
data['middle_avg_bill'] = pd.DataFrame(data=imputer.fit_transform(middle_avg_bill_2d),
                           columns=['middle_avg_bill'],
                           index=data.index)
In [ ]:
middle_avg_bill_2d = data['middle_coffee_cup'].values.reshape(-1, 1)
data['middle_coffee_cup'] = pd.DataFrame(data=imputer.fit_transform(middle_avg_bill_2d),
                           columns=['middle_coffee_cup'],
                           index=data.index)

Комментарий:

Столбец middle_coffee_cup содержит число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена чашки капучино». Если значения в столбце avg_bill нет или оно не начинается с подстроки «Цена чашки капучино», то в столбец ничего не войдёт.

Выведем количество непустых значений столбца middle_coffee_cup.

In [ ]:
len(data.query('~middle_coffee_cup.isna()'))

Найдём количество строк датафрейма data, для которых значение столбца avg_bill начинается с подстроки «Цена чашки капучино».

In [ ]:
len(data.loc[data['avg_bill'].apply(lambda x: x.find('цена чашки капучино')) != -1])

Комментарий:

Число строк 535 равно количеству непустых значений в столбце middle_coffee_cup.

Заполним пропуски в столбце middle_coffee_cup заглушкой "0".

In [ ]:
#data['middle_coffee_cup'] = data['middle_coffee_cup'].fillna(0)

Заменим пропуски в столбце seats заглушкой "-1".

In [ ]:
#data['seats'] = data['seats'].fillna(-1)
In [ ]:
# Проверим обработку пропусков.
data.isna().sum()
In [ ]:
# Посмотрим на дубликаты
data.loc[data.duplicated(subset=['name', 'address'], keep=False)]

Комментарий:
Явным дубликатом выглядит вторая строка с данными ресторана "More Poke" и "кафе" Вторые строки для заведений "Раковарня клешни и хвосты" и "Хлеб да выпечка" содержат разные категории заведений и немного разные координаты. Так как адрес один, будем считать это дубликатами и удалим дублирующие строки.

In [ ]:
data.drop_duplicates(subset=['name', 'address'], keep='first', inplace=True)
In [ ]:
data.duplicated(subset=['name', 'address']).sum()

Проверим на неявные дубликаты названия категорий заведений и районы.

In [ ]:
data['category'].unique()
In [ ]:
data['district'].unique()

Неявных дубликатов в столбцах category и district нет. Выведем топ-10 самых часто встречающихся названий заведений.

In [ ]:
data.groupby('name') \
    .agg(count=('name', 'count')) \
    .sort_values(by='count', ascending=False) \
    .reset_index() \
    .head(10) \
    .style.background_gradient('coolwarm')

Комментарий:
Очевидно, что большая часть из этих заведний - сетевые. Но встречаются и большое количество заведений с названиями "Кафе", "Ресторан" и "Столовая". Проверим, есть ли среди заведений с такими названиями сетевые.

In [ ]:
data.query('(name == "кафе" or name == "ресторан" or name == "столовая") and chain == 1')

Комментарий:
Сетевых нет. Необходимо проверить данные на неявные дубликаты по названиям.

Попробуем поискать неявные дубликаты по первому слову в названии заведения. Для этого создадим столбец с первым словом в названии и поищем дубликаты по этому столбцу, столбцу category и address.

In [ ]:
data['dup_name'] = data['name'].str.split(' ').str[0]
data.duplicated(subset=['dup_name', 'category', 'address']).sum()

Обнаружено 15 дубликатов. Выведем строки с дубликатами.

In [ ]:
data.loc[data.duplicated(subset=['dup_name', 'category', 'address'], keep=False)]

Комментарий:

У этих заведений похожие названия, совпадают категории и адреса. По-видимому, некоторые из них представляют собой разные заведения, находящиеся в одном здании, либо разные залы одного заведения. Часть заведений является дубликатами. Удалим строки, которые по всей видимости являются дубликатами.

In [ ]:
data = data.drop(data[(data['name'] == "чебуреки манты") & (data['address'] == "Москва, Правобережная улица, 1Б")].index)
data = data.drop(data[(data['name'] == "чайхана халал") & (data['address'] == "Москва, Смольная улица, 24Г, стр. 6")].index)
data = data.drop(data[(data['name'] == "dragon bubble tea") & (data['address'] == "Москва, Щёлковское шоссе, вл75")].index)
data = data.drop(data[(data['name'] == "баку 24 часа") & (data['address'] == "Москва, Монтажная улица, 9, стр. 1")].index)
data = data.drop(data[(data['name'] == "udcкафе upside down cake") & (data['address'] == "Москва, Кутузовский проспект, 57")].index)
data = data.drop(data[(data['name'] == "vip wok and sushi") & (data['address'] == "Москва, Можайское шоссе, 45Б")].index)
data = data.drop(data[(data['name'] == "от мяса до рыбы") & (data['address'] == "Москва, улица Вавилова, 64/1с1")].index)
data = data.drop(data[(data['name'] == "чайхана дружба") & (data['address'] == "Москва, Большая Очаковская улица, 47А, стр. 1")].index)
data = data.drop(data[(data['name'] == "estetica cafe") & (data['address'] == "Москва, Кировоградская улица, 15")].index)
data = data.drop(data[(data['name'] == "кафе") & (data['address'] == "Москва, Ореховый бульвар, 28")].index)
data.loc[data.duplicated(subset=['dup_name', 'category', 'address'], keep=False)]

Дубликаты обработаны. Удалим столбец dup_name.

In [ ]:
data = data.drop(['dup_name'], axis=1)
In [ ]:
# Изменим тип данных на int
#for col in ['middle_avg_bill', 'middle_coffee_cup', 'seats']:
    #data[col] = data[col].astype('int')
In [ ]:
def make_acronym(phrase):
    phrase = phrase.replace('-', ' ').split()
    acronym = ""
    for word in phrase:
        acronym = acronym + word[0].upper()
    return acronym

# Добавим акронимы к районам
data['district_short'] = data['district'].apply(make_acronym)

Анализ данных¶

Категории заведений¶

Посмотрим какие категории заведений представлены в данных. Исследуем количество объектов общественного питания по категориям: рестораны, кофейни, пиццерии, бары и так далее. Построим визуализации.

In [ ]:
category_name = data.groupby('category')['name'].count().reset_index()
category_name.columns = ['category', 'count']
category_name.style.background_gradient('coolwarm')
In [ ]:
#fig = go.Figure(data=[go.Bar(x=category_name['category'], y=category_name['count'])], 
#               layout=go.Layout(title=go.layout.Title(text="Столбчатая диаграмма категорий заведений"),
#                                xaxis=go.layout.XAxis(title=go.layout.xaxis.Title(text="Категории")),
#                                yaxis=go.layout.YAxis(title=go.layout.yaxis.Title(text="Количество заведений")), 
#                                template='plotly_white'))

#fig.show()
In [ ]:
fig = px.bar(category_name, 
             x='category', 
             y='count', 
             text='count',
             title='Количество объектов общественного питания по видам',
             template='plotly_white'
            )
fig.update_layout(xaxis_title='Категории заведений',
                  yaxis_title='Количество заведений',
                  xaxis={'categoryorder':'total descending'})
fig.show()

Комментарий:

кафе с наибольшим количеством - 2376 заведений, за ним следуют рестораны с 2042 заведениями, а наименьшее число заведений у булочных - всего 256. Однако, кафе и рестораны составляют более чем половину (около 52,6%) от общего числа заведений, в то время как кофейни, бары/пабы и пиццерии представлены чуть менее чем третью частью (33,43%). Оставшиеся заведения, такие как булочные, столовые и быстрое питание занимают 13,97% от общего числа.

Исследование количества посадочных мест в местах по категориям.¶

Исследуем количество посадочных мест в местах по категориям: рестораны, кофейни, пиццерии, бары и так далее. Построим визуализации.

In [ ]:
seats = data.query('seats != -1') \
            .groupby('category') \
            .agg(seats_median=('seats', 'median')) \
            .sort_values(by='seats_median', ascending=False) \
            .reset_index()
seats['seats_median'] = seats['seats_median'].astype('int')
In [ ]:
# строим столбчатую диаграмму 
fig = px.bar(seats.sort_values(by='seats_median', ascending=True), # загружаем данные и заново их сортируем
             x='seats_median', # указываем столбец с данными для оси X
             y='category', # указываем столбец с данными для оси Y
             text='seats_median',
             template='plotly_white'# добавляем аргумент, который отобразит текст с информацией
                                # о количестве объявлений внутри столбца графика
            )
# оформляем график
fig.update_layout(title='Количество посадочных мест в объектах общественного питания по категориям',
                   xaxis_title='Количество посадочных мест',
                   yaxis_title='Категория объекта общественного питания')
fig.show() # выводим график

Комментарий:

Рестораны предоставляют наибольшее количество мест для посадки, что логично, поскольку такая специализация предполагает отдых и питание гостей. На втором месте - быстрые рестораны, при этом они являются самыми распространенными заведениями. В то же время, кафе и столовые предлагают наименьшее количество мест для посадки.

Cоотношение сетевых и несетевых заведений в датасете¶

Подсчитаем число сетевых и несетевых заведений.

In [ ]:
chain_category = ['сетевые', 'несетевые']
values = [len(data.query('chain == 1')), len(data.query('chain == 0'))]

fig = go.Figure(data=[go.Pie(labels=chain_category, values=values)])
fig.update_layout(title='Cоотношение сетевых и несетевых заведений общественного питания', 
                  width=800, 
                  height=500,
                  annotations=[dict(x=1.15, 
                                    y=1.05,
                                    text='Категория',
                                    showarrow=False)])
fig.show() 

Комментарий:

Судя по диаграмме, можно утверждать, что примерно 62% ресторанов в Москве не принадлежат к какой-либо сети.

Категории сетевых заведений общественного питания¶

In [ ]:
# Посмотрим какие категории заведений чаще являются сетевыми
chain_objects = data.groupby(['category', 'chain'])['name'].count().reset_index()
chain_objects.columns = ['object_type', 'chain', 'count']
chain_objects['chain'] = chain_objects['chain'].astype(object)
chain_objects = chain_objects.sort_values(['count', 'chain'])
chain_objects
In [ ]:
fig = px.bar(chain_objects,
             x='count',
             y='object_type',
             text='count',
             template='plotly_white',                   
             color='chain',
             category_orders={"chain": ["сетевой", "несетевой"]}
            )
# оформляем график
fig.update_layout(title='Соотношение сетевых заведений',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название категорий',
                 )
fig.show()

Комментарий:

Согласно графику, большинство заведений не относятся к сетевым, за исключением нескольких категорий: для кофеен количество сетевых заведений немного превышает количество независимых (720|693); для пиццерий количество сетевых заведений уже значительно превышает количество независимых (330|303); для булочных разница между сетевыми и независимыми заведениями оказалась самой большой (157|99).

Топ-15 популярных сетей в Москве¶

Сгруппируем данные по названиям заведений и найдем топ-15 популярных сетей в Москве.

In [ ]:
df_chain = data[data['chain'] == 1]
top_15 = df_chain.groupby('name').agg({'rating' : 'median', 'category' : pd.Series.mode, 'district' : 'count'})
top_15 = top_15.rename(columns={'district':'count'})
top_15 = top_15.sort_values('count', ascending = False).reset_index().head(15)
top_15
In [ ]:
fig = px.bar(top_15,
             x='count',
             y='name',
             text='count',
             template='plotly_white', 
             color='name'
            )
# оформляем график
fig.update_layout(title='ТОП-15 популярных сетей в Москве',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название заведений',
                   showlegend=False)
fig.show()

Комментарий:

По результатам мы можем заключить, что среди сетевых заведений в Москве первое место занимает Шоколадница, второе и третье места заняли известные пиццерии, а наименьшее количество заведений относится к сети Му-Му.

In [ ]:
print('Всего заведений в Топ-15:', top_15['count'].sum())
In [ ]:
fig = px.bar(top_15,
             x='count',
             y='category',
             template='plotly_white',                 
             color='category'
            )
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название категорий',
                   yaxis={'categoryorder':'total ascending'}
                 )
fig.show()
In [ ]:
district_chain = df_chain.groupby(['district', 'category', 'name']).agg({'rating' : 'median', 'address' : 'count'})
district_chain = district_chain.sort_values('rating', ascending = False).reset_index()
district_chain = district_chain.rename(columns={'address':'count'})
district_chain = district_chain[district_chain['name'].isin(top_15['name'])]
district_chain.head()
In [ ]:
fig = px.bar(district_chain,
             x='count',
             y='district',
             template='plotly_white',                    
             color='category'
            )
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название района',
                   yaxis={'categoryorder':'total ascending'}
                 )
fig.show()

Какие административные районы Москвы присутствуют в датасете?¶

Посмотрим какие административные районы Москвы присутствуют в датасете. Отобразим общее количество заведений и количество заведений каждой категории по районам.

In [ ]:
print('Общее количество заведений в датасете:',data['name'].count())
In [ ]:
district_chain_df = data.groupby(['district', 'category']).agg({'rating' : 'median', 'name' : 'count'})
district_chain_df = district_chain_df.sort_values('rating', ascending = False).reset_index()
district_chain_df = district_chain_df.rename(columns={'name':'count'})
district_chain_df.head()
In [ ]:
fig = px.bar(district_chain_df,
             x='count',
             y='district',
             template='plotly_white',                    
             color='category'
            )
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название района',
                   yaxis={'categoryorder':'total ascending'}
                 )
fig.show()

Комментарий:

Мы имеем 9 административных округов. График показывает, что наибольшее количество заведений всех категорий находится в центральном административном округе. В основном, это кафе, кофейни и рестораны, что логично, учитывая, что люди едут в центр города, чтобы отдохнуть, погулять или сделать покупки. Категория кафе имеет неплохое распределение по всем округам. В то же время столовых наименьшее количество в каждом из округов.

Средние рейтинги по категориям¶

Визуализируем распределение средних рейтингов по категориям заведений.

In [ ]:
rating_category = data.groupby('category').agg({'rating' : 'mean'}).round(2).sort_values('rating', ascending = False).reset_index()
rating_category
In [ ]:
fig = px.bar(rating_category,
             x='rating',
             y='category',
             text='rating',
             template='plotly_white', 
             color='category'                   
            )
# оформляем график
fig.update_layout(title='Распределение средних рейтингов по категориям заведений',
                   xaxis_title='Рейтинг',
                   yaxis_title='Название категорий')
fig.update_xaxes(range=[4, 4.5])
fig.show()

Комментарий:

Согласно данным на графике, бары и пабы имеют наивысший рейтинг, тогда как пиццерии, рестораны, кофейни и булочные имеют примерно одинаковый рейтинг. Рестораны быстрого питания имеют наименьший рейтинг. Кроме того, средние значения по всем категориям превышают 4 в нашей выборке.

Фоновая картограмма (хороплет) со средним рейтингом заведений каждого района¶

Построим фоновую картограмму (хороплет) со средним рейтингом заведений каждого района. Границы районов Москвы, которые встречаются в датасете, хранятся в файле admin_level_geomap.geojson

In [ ]:
rating_df = data.groupby('district', as_index=False)['rating'].agg('mean').round(2)
rating_df
In [ ]:
# читаем файл и сохраняем в переменной
with open('/datasets/admin_level_geomap.geojson', 'r') as f:
    geo_json = json.load(f)
In [ ]:
# загружаем JSON-файл с границами округов Москвы
state_geo = '/datasets/admin_level_geomap.geojson'
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423

# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10)

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=rating_df,
    columns=['district', 'rating'],
    key_on='feature.name',
    #fill_color='YlGn',
    #fill_opacity=0.8,
    legend_name='Средний рейтинг заведений по районам',
).add_to(m)

# выводим карту
m

На основании предоставленных данных можно сделать вывод о том, что у заведений, расположенных в Центральном административном округе, наивысший рейтинг, составляющий 4.38. В то же время заведения, расположенные в Юго-Восточном административном округе, имеют наименьший рейтинг, который составляет 4.1.

Заведения датасета на карте¶

Отобразим все заведения датасета на карте с помощью кластеров средствами библиотеки folium.

In [ ]:
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423

# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m)

# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['rating']}",
    ).add_to(marker_cluster)

# применяем функцию create_clusters() к каждой строке датафрейма
data.apply(create_clusters, axis=1)

# выводим карту
m

По карте отчетливо видно, что основная масса заведений сконцентрирована в центре Москвы. Уже меньше на севере и еще меньше на юге.

Топ-15 улиц по количеству заведений¶

Найдем топ-15 улиц по количеству заведений. Построим график распределения количества заведений и их категорий по этим улицам.

In [ ]:
words = ['проезд','шоссе','улица','переулок','микрорайон','мкад','проспект','пр.',
         'площадь','аллея','бульвар','набережная','сквер','тупик','линия','территория',
         'квартал','просек','парк','мост']
 
str_pat = r".*,\s*\b([^,]*?(?:{})\b[^,]*)[,$]+".format("|".join(words))
 
data['street'] = data['address'].str.extract(str_pat, flags=re.I)
In [ ]:
streets_moscow = data[data['street'].notnull()]
top15_streets = streets_moscow['street'].value_counts().reset_index().head(15)
top15_streets.columns = ['street_name', 'count']
top15_streets
In [ ]:
#создадим таблицу с названиями улиц и категорий
streets_category = data.groupby(['street', 'category'])['name'].count().reset_index()
streets_category.columns = ['street_name', 'category', 'count']
streets_category.sort_values('count', ascending=False)
In [ ]:
#оставим только улицы из топ 15
streets15_category = streets_category[streets_category['street_name'].isin(top15_streets['street_name'])]
streets15_category
In [ ]:
fig = px.bar(streets15_category,
             x='count',
             y='street_name',
             template='plotly_white',                   
             color='category'
            )
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название улиц',
                   yaxis={'categoryorder':'total ascending'}
                 )
fig.show()

Из графика выше мы видим, что больше всего заведений на проспекте Мира. Преобладают категории кафе и рестораны. Столовых меньше всего. Следом за ним идет Профсоюзная, ситуация с категориями там аналогична. Меньше всего заведений на улица Миклухо-Маклая. Среди популярных категорий также кафе и рестораны.

In [ ]:
one_cafe = data['street'].value_counts().reset_index()
one_cafe.columns = ['street_name', 'cafe_count']
one_cafe = one_cafe[one_cafe['cafe_count'] == 1]
one_cafe
In [ ]:
#добавим районы
streets1_category = streets_category[streets_category['street_name'].isin(one_cafe['street_name'])]
streets1_category = streets1_category.groupby('category')['street_name'].count()
streets1_category

Из данных выше мы видим, что 425 улиц имеют только одно заведение. Больше всего из них относятся к категории кафе.

Значения средних чеков заведений¶

Значения средних чеков заведений хранятся в столбце middle_avg_bill. Эти числа показывают примерную стоимость заказа в рублях, которая чаще всего выражена диапазоном. Посчитаем медиану этого столбца для каждого района. Используем это значение в качестве ценового индикатора района. Построим фоновую картограмму (хороплет) с полученными значениями для каждого района.

In [ ]:
median_bill = data.groupby('district')['middle_avg_bill'].median().reset_index()
In [ ]:
# создаём карту Москвы
m2 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=median_bill,
    columns=['district', 'middle_avg_bill'],
    key_on='feature.name',
    legend_name='Средний чек заведений по районам',
).add_to(m2)

# выводим карту
m2

Комментарий:

Из приведенных данных можно сделать вывод, что Центральный и Западный округа имеют наибольший средний чек в сравнении с Юго-Восточным округом, который имеет наименьший средний чек. Округи вблизи Центрального округа имеют в среднем средний чек на 1,5 - 2 раза выше, чем округа вдали от центра.

Вывод¶

Анализ показал, что наибольшее количество заведений в Москве - кафе (2378), немного меньше - рестораны (2043), и наименьшее количество - булочные (256).

Кафе и рестораны составляют почти 52,6% от общего числа заведений, в то время как кофейни, бары/пабы и пиццерии составляют 33,43%, а булочные, столовые и заведения быстрого питания - 13,97%. Рестораны предоставляют наибольшее количество посадочных мест, а заведения быстрого питания распространены шире всего. Кофейни занимают первое место по количеству сетевых заведений, за которой следует категория пиццерий.

Самой популярной сетью является Шоколадница. Заведений категории кофе, ресторанов и пиццерий в топ-15 примерно одинаковое количество. Заведений из топ-15 в центральном административном округе больше всего, а в северо-западном - наименьшее количество.

Проспект Мира - улица с наибольшим количеством заведений, преобладают кафе и рестораны, а меньше всего - столовые. Средний чек выше всего в центральном и западном округах, а наименьший - в юго-восточном.

Детализируем исследование: открытие кофейни¶

Ответим на следующие вопросы:
Сколько всего кофеен в датасете? В каких районах их больше всего, каковы особенности их расположения?
Есть ли круглосуточные кофейни?
Какие у кофеен рейтинги? Как они распределяются по районам?
На какую стоимость чашки капучино стоит ориентироваться при открытии и почему?

Количество кофеен¶

Посчитаем количество и посмотрим на их расположение.

In [ ]:
cofe_df = data[data['category'] == 'кофейня']
print('Всего коффен:', cofe_df.shape[0])
In [ ]:
# создаём карту Москвы
m3 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m3)

# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['rating']}",
    ).add_to(marker_cluster)

# применяем функцию create_clusters() к каждой строке датафрейма
cofe_df.apply(create_clusters, axis=1)

# выводим карту
m3

У нас имеется 1413 кофейных мест. Большинство из них расположены в Центральном районе. За ними следуют юго-западные и северные части города.

Время работы¶

Посмотрим, есть ли круглосуточные кофейни.

In [ ]:
coffee = data.query('hours == "ежедневно, круглосуточно" & category == "кофейня"')
coffee_house = data.query('hours == "ежедневно, круглосуточно" & category == "кофейня"') \
                   .groupby(by=['district_short'], as_index=False) \
                   .agg(count=('name', 'count'))
print(f'Количество кругосуточных кофеен: {coffee_house["count"].sum()}')
In [ ]:
fig = px.bar(
    data_frame=coffee_house.sort_values('count',ascending=False),
    x='district_short', y='count', color='district_short', text='count',
    title='Количество кофеен 24/7 по районам',
    labels={'district_short': 'Район', 'count': 'Количество кафе'},
    height=450
)

fig.update_layout(
    legend_title='Часы работы',
    template='plotly_white'
)
fig.show()
In [ ]:
# создаём карту Москвы
m4 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m4)

# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['rating']}",
    ).add_to(marker_cluster)

# применяем функцию create_clusters() к каждой строке датафрейма
coffee.apply(create_clusters, axis=1)

# выводим карту
m4

Комментарий:

Из этого можно сделать вывод, что в центре города наиболее распространены круглосуточные кофейни, в то время как в остальных районах это отсутствует или представлено в минимальном количестве.

Рейтинги кофеен¶

Посмотрим какие у кофеен рейтинги. Как они распределяются по районам.

In [ ]:
rating_cofe = cofe_df.groupby('district', as_index=False)['rating'].agg('mean').round(2).sort_values('rating', ascending=False)
rating_cofe
In [ ]:
# создаём карту Москвы
m5 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=rating_cofe,
    columns=['district', 'rating'],
    key_on='feature.name',
    #fill_color='YlGn',
    #fill_opacity=0.8,
    legend_name='Средний рейтинг заведений по районам',
).add_to(m5)

# выводим карту
m5

Комментарий:
Центральный административный округ и Северо-Западный административный округ имеют самые высокие показатели рейтинга по сравнению с другими округами, в то время как Западный административный округ имеет наименьший рейтинг.

Cтоимость чашки капучино¶

Посмотрим на какую стоимость чашки капучино стоит ориентироваться при открытии.

In [ ]:
middle_cofe = cofe_df.groupby('district', as_index=False)['middle_coffee_cup'].agg('mean').round().sort_values('middle_coffee_cup', ascending=False)
display(middle_cofe)
print('Средняя стоимость чашки кофе в Москве:', middle_cofe['middle_coffee_cup'].mean().round())
In [ ]:
# создаём карту Москвы
m6 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=middle_cofe,
    columns=['district', 'middle_coffee_cup'],
    key_on='feature.name',
    #fill_color='YlGn',
    #fill_opacity=0.8,
    legend_name='Средний цена чашки кофе по районам',
).add_to(m6)

# выводим карту
m6

Комментарий:

В Центральном, Западном и Юго-Западном округах находится самый дорогой кофе, средняя его стоимость составляет 180, в то время как в среднем по Москве цена за чашку кофе равна 171. Если вы собираетесь открыть новое кафе, рекомендуется определить цену на кофе исходя из района, в котором находится ваше заведение, не превышая средней цены в этом районе.

Рекомендация¶

При запуске новой кофейни рекомендуется выбрать Центральный, Западный или Юго-Западный округа, поскольку здесь средняя стоимость кофе выше, что позволит получить хорошую прибыль. Это также позволит начать дело со сниженной ценой на кофе без больших потерь.

Стоит обдумать возможность круглосуточной работы. Западный и Юго-Западный округа являются дефицитными регионами в плане круглосуточных заведений. В Центральном округе будет лучше воспользоваться форматом 24/7, так как это самый оживленный район с большим количеством пешеходных улиц и оживлен в темное время суток. Кроме того, на западных округах наблюдается низкий рейтинг заведений, что может быть использовано как преимущество при запуске.

Презентация¶

Ссылка: https://disk.yandex.ru/i/HGCwRNmm7LF2Pw

In [ ]: